/************************************************************************
 *
 * Copyright (c) 2009
 * PrismTech Ltd.
 * All rights Reserved.
 *
 * LOGICAL_NAME:    BattlefieldApp.java
 * FUNCTION:        OpenSplice DDS Power Tools Tutorial example code.
 * MODULE:          Tutorial for the Java programming language.
 * DATE             April 2009.
 ************************************************************************
 *
 * This class contains the implementation of the battlefield application.
 *
 ***/

package Battlefield;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import track.ArrayListComparator;
import track.ArrayListTableModel;
import track.ArrayListTableUpdater;
import track.BaseApplication;
import track.Classification_enum;
import track.Constants;
import track.ErrorHandler;
import track.ObjectLocation;
import track.ObjectLocationDataWriter;
import track.TrackState;
import track.TrackStateDataReader;
import track.TrackStateSeqHolder;
import Battlefield.BattlefieldAppWrapper.WrapperException;
import DDS.ALIVE_INSTANCE_STATE;
import DDS.ANY_SAMPLE_STATE;
import DDS.ANY_VIEW_STATE;
import DDS.HANDLE_NIL;
import DDS.LENGTH_UNLIMITED;
import DDS.SampleInfo;
import DDS.SampleInfoSeqHolder;

public class BattlefieldApp extends BaseApplication {
   // Interface elements.
   public static JFrame m_frame;
   private static JButton m_startButton;
   private static JButton m_stopButton;
   private static JButton m_exitButton;

   // Minimum and maximum altitude for airborne tracked objects.
   public static int MIN_ALTITUDE = 15;
   public static int MAX_ALTITUDE = 100;

   // For a new tracked object, the chance (0-1) that it will be surface-level
   // versus airborne.
   public static float SURFACE_LEVEL_CHANCE = 0.4F;

   // Maximum and minimum velocity values for a new tracked object.
   public static int MIN_DELTA_X = -2;
   public static int MAX_DELTA_X = 2;
   public static int MIN_DELTA_Y = -2;
   public static int MAX_DELTA_Y = 2;

   // Current number of tracked objects (and default).
   public static int m_currentTracked = 10;

   // Absolute minimum of tracked objects.
   public static int MINIMUM_TRACKED = 1;

   // Absolute maximum number of tracked objects.
   public static int MAXIMUM_TRACKED = 50;

   // Current delay between battlefield updates (and default).
   public static int m_battlefieldUpdateDelay = 100;

   // Minimum delay between battlefield updates.
   public static int BATTLEFIELD_MINIMUM_UPDATE_DELAY = 100;

   // Minimum delay between battlefield updates.
   public static int BATTLEFIELD_MAXIMUM_UPDATE_DELAY = 10000;

   // Sequentially-allocated unique track ID.
   private static int TRACK_ID;

   // Default transport priority for the ObjectLocation data writer.
   public static int DEFAULT_TRANSPORT_PRIORITY = 3;

   // Default latency budget for the ObjectLocation data writer.
   public static double DEFAULT_LATENCY_BUDGET = 0D;

   // ObjectLocation message padding.
   public static int DEFAULT_PADDING = 0;
   public static String m_padding;

   // Collection of tracked objects; key is the track ID.
   private HashMap<Integer, TrackedObject> m_tracked;

   // The ObjectLocation data writer reference.
   private ObjectLocationDataWriter m_objectLocationDataWriter;
   
   // The TrackState data reader listener reference.
   private TrackStateDataReaderListenerImpl m_tsdrListener;

   // Reference to the updater.
   private BattlefieldUpdater m_updater;

   // Reference to the thread running the updater.
   private Thread m_updaterThread;

   // Flag to indicate if updating should be performed or not.
   private boolean m_performUpdating = false;

   // References for the table used to display the current battlefield objects.
   private static JTable m_table;
   private static ArrayListTableModel m_tableModel;
   private static String[] m_tableHeaders = { "ID", "X", "X Velocity", "Y",
         "Y Velocity", "Height" };

   /**
    * Data for a tracked object.
    */
   class TrackedObject {
      // Track ID. While this is also held in the ObjectLocation instance, the
      // battlefield assigns an ID separately from creating and registering
      // the
      // topic instance.
      int m_id;

      // Current position.
      int m_x;
      int m_y;
      int m_z;

      // Velocity.
      int m_deltax;
      int m_deltay;

      // Battlefield topic instance and associated handle.
      ObjectLocation m_objLoc = null;
      long m_handle = 0L;

      // Destroyed flag, set to true if the object is destroyed.
      boolean destroyed = false;
      
      public String toString() {
         StringBuffer sb = new StringBuffer("ID: ");
         sb.append(m_id);
         sb.append(", POS: ");
         sb.append(m_x);
         sb.append(" (");
         sb.append(m_deltax);
         sb.append("), ");
         sb.append(m_y);
         sb.append(" (");
         sb.append(m_deltay);
         sb.append("), ");
         sb.append(m_z);

         return sb.toString();
      }
   }

   /**
    * Updater thread.
    */
   class BattlefieldUpdater implements Runnable {
      boolean m_keepGoing = true;

      private void delay(long duration) {
         if (duration != 0) {
            try {
               synchronized (this) {
                  wait(duration);
               }
            } catch (InterruptedException ie) {
               System.out.println("Delay of " + duration
                     + "ms interrupted");
            }
         }
      }

      public String toString() {
         return "BattlefieldUpdater";
      }

      public void run() {
         int status;

         while (m_keepGoing) {
            // Perform the required delay.
            delay(m_battlefieldUpdateDelay);

            // If updating is not in operation, continue.
            if (!m_performUpdating) {
               continue;
            }

            // Update all tracked objects.
            synchronized (m_syncObject) {
               synchronized (m_tracked) {
                  ArrayList<ArrayList<Integer>> tableData = new ArrayList<ArrayList<Integer>>();

                  Set<Integer> keys = m_tracked.keySet();
                  Iterator<Integer> keysIter = keys.iterator();
                  while (keysIter.hasNext()) {
                     Integer key = (Integer) keysIter.next();
                     TrackedObject tracked = (TrackedObject) m_tracked
                           .get(key);

                     // Update the position.
                     tracked.m_x += tracked.m_deltax;
                     tracked.m_y += tracked.m_deltay;

                     ArrayList<Integer> tableRow = new ArrayList<Integer>();
                     tableRow.add(new Integer(tracked.m_id));
                     tableRow.add(new Integer(tracked.m_x));
                     tableRow.add(new Integer(tracked.m_deltax));
                     tableRow.add(new Integer(tracked.m_y));
                     tableRow.add(new Integer(tracked.m_deltay));
                     tableRow.add(new Integer(tracked.m_z));
                     tableData.add(tableRow);

                     if ((tracked.destroyed == true) || 
                         (tracked.m_x < Constants.BATTLEFIELD_MIN_X
                           || tracked.m_x > Constants.BATTLEFIELD_MAX_X
                           || tracked.m_y < Constants.BATTLEFIELD_MIN_Y
                           || tracked.m_y > Constants.BATTLEFIELD_MAX_Y)) {
                        // Passed beyond the battlefield boundary or destroyed:
                        // remove the object.
                        keysIter.remove();
                        if (DEBUG) {
                           output("Tracked object "
                                 + tracked.toString()
                                 + " has left the battlefield");
                        }

                        // If the instance was registered with the
                        // topic, dispose and unregister it.
                        if (tracked.m_objLoc != null) {
                           status = m_objectLocationDataWriter
                                 .dispose(tracked.m_objLoc,
                                       tracked.m_handle);
                           ErrorHandler.checkStatus(status,
                                 "BattlefieldUpdater.run: dispose");
                           status = m_objectLocationDataWriter
                                 .unregister_instance(
                                       tracked.m_objLoc,
                                       tracked.m_handle);
                           ErrorHandler
                                 .checkStatus(status,
                                       "BattlefieldUpdater.run: unregister_instance");

                           if (DEBUG) {
                              output("Disposed/unregistered object ID "
                                    + tracked.m_id);
                           }
                        }
                     } else {
                        // Object still on the battlefield; publish the
                        // information to the battlefield topic.

                        // Create and register the ObjectLocation
                        // instance if needed.
                        if (tracked.m_objLoc == null) {
                           tracked.m_objLoc = new ObjectLocation();
                           tracked.m_objLoc.trackId = tracked.m_id;
                           tracked.m_objLoc.padding = m_padding;

                           tracked.m_handle = m_objectLocationDataWriter
                                 .register_instance(tracked.m_objLoc);
                           if (tracked.m_handle == HANDLE_NIL.value) {
                              output("Error registering object ID "
                                    + tracked.m_objLoc.trackId);
                           }

                           if (DEBUG) {
                              output("Registered object ID "
                                    + tracked.m_id);
                           }
                        }

                        // Update the position information and publish
                        // to the topic.
                        tracked.m_objLoc.x = tracked.m_x;
                        tracked.m_objLoc.y = tracked.m_y;
                        tracked.m_objLoc.z = tracked.m_z;
                        status = m_objectLocationDataWriter.write(
                              tracked.m_objLoc, tracked.m_handle);
                        ErrorHandler.checkStatus(status,
                              "BattlefieldUpdater.run: write");
                     }
                  }

                  // Ensure we track the requested number of objects.
                  while (m_tracked.size() < m_currentTracked) {
                     TrackedObject newObject = createTracked();

                     if (DEBUG) {
                        output("Created new tracked object: ID "
                              + newObject.m_id
                              + ", current map size "
                              + m_tracked.size() + ", maximum "
                              + m_currentTracked);
                     }
                  }

                  // Ensure the information table data is sorted by object
                  // ID.
                  ArrayListComparator comp = new ArrayListComparator();
                  Collections.sort(tableData, comp);

                  // Update the table.
                  SwingUtilities.invokeLater(new ArrayListTableUpdater(
                        m_tableModel, tableData));
               }
            }
         }
      }
   }
   
   /**
    * Listener implementation for the TrackState data reader.
    */
   class TrackStateDataReaderListenerImpl extends
   		BattlefieldAppWrapper.TrackStateDataReaderListener {
      public void on_data_available(DDS.DataReader dataReader) {
         TrackStateDataReader tsDataReader = (TrackStateDataReader) dataReader;
         TrackStateSeqHolder receivedData = new TrackStateSeqHolder();
         SampleInfoSeqHolder infoSeq = new SampleInfoSeqHolder();
         int status;

         synchronized (m_syncObject) {
        	 status = tsDataReader.take(receivedData, infoSeq,
        		LENGTH_UNLIMITED.value, ANY_SAMPLE_STATE.value,
                ANY_VIEW_STATE.value, ALIVE_INSTANCE_STATE.value);
            ErrorHandler
                  .checkStatus(status,
                        "TrackStateDataReaderListenerImpl: on_data_available: take");

            TrackState[] states = receivedData.value;
            SampleInfo[] samples = infoSeq.value;
            for (int i = 0; i < states.length; i++) {
               TrackState state = states[i];
               SampleInfo sample = samples[i];
               if (sample.instance_state == DDS.ALIVE_INSTANCE_STATE.value) {
            	  if (state.classification.value() == Classification_enum._TRACK_CLASSIFICATION_DESTROYED) {
                      // Update the tracked object.
                      synchronized (m_syncObject) {
                         synchronized (m_tracked) {
                        	 TrackedObject tracked = m_tracked.get(Integer.valueOf(state.trackId));
                        	 tracked.destroyed = true;
                         }
                      }
            	  }
               }
            }

            // Release resources.
            status = tsDataReader.return_loan(receivedData, infoSeq);
            ErrorHandler
                  .checkStatus(status,
                        "TrackStateDataReaderListenerImpl: on_data_available: return_loan");
         }
      }
   }

   public BattlefieldApp(String[] args) {
      parseArguments(args);

      m_tracked = new HashMap<Integer, TrackedObject>(MAXIMUM_TRACKED);
      m_updater = new BattlefieldUpdater();
      m_updaterThread = new Thread(m_updater);
      m_updaterThread.start();

      // Obtain a reference to the ObjectLocation data writer.
      m_objectLocationDataWriter = BattlefieldAppWrapper.ParticipantWrapper.PublisherWrapper.ObjectLocationDataWriterWrapper
            .getDataWriter();
      
      // Create and attach the listeners.
      m_tsdrListener = new TrackStateDataReaderListenerImpl();
      try {
         BattlefieldAppWrapper.BattleshipParticipantWrapper.SubscriberWrapper.TrackStateDataReaderWrapper
               .attach(m_tsdrListener);
      } catch (WrapperException e) {
         System.err
               .println("Listener cannot be attached: " + e.getMessage());
      }
   }

   /**
    * Creates a new tracked object. New objects always appear at an edge, and
    * have a velocity that will take them into the battlefield.
    */
   private TrackedObject createTracked() {
      TrackedObject tracked = new TrackedObject();
      tracked.m_id = TRACK_ID++;

      // Ensure at least one velocity is not zero.
      int deltax = 0;
      int deltay = 0;
      while (deltax == 0 && deltay == 0) {
         deltax = MIN_DELTA_X
               + (int) (m_random.nextFloat() * (MAX_DELTA_X - MIN_DELTA_X));
         deltay = MIN_DELTA_Y
               + (int) (m_random.nextFloat() * (MAX_DELTA_Y - MIN_DELTA_Y));
      }

      // Assign a random starting point and appropriate velocity.
      if (m_random.nextFloat() < 0.50) {
         // Random X position, minimum/maximum Y position.
         tracked.m_x = Constants.BATTLEFIELD_MIN_X
               + (int) (m_random.nextFloat() * (Constants.BATTLEFIELD_MAX_X - Constants.BATTLEFIELD_MIN_X));
         tracked.m_y = (m_random.nextFloat() < 0.5 ? Constants.BATTLEFIELD_MIN_Y
               : Constants.BATTLEFIELD_MAX_Y);

         tracked.m_deltax = deltax;
         tracked.m_deltay = (tracked.m_y == Constants.BATTLEFIELD_MIN_Y ? Math
               .abs(deltay)
               : Math.abs(deltay) * -1);
      } else {
         // Minimum/maximum X position, random Y position.
         tracked.m_x = (m_random.nextFloat() < 0.5 ? Constants.BATTLEFIELD_MIN_X
               : Constants.BATTLEFIELD_MAX_X);
         tracked.m_y = Constants.BATTLEFIELD_MIN_Y
               + (int) (m_random.nextFloat() * (Constants.BATTLEFIELD_MAX_Y - Constants.BATTLEFIELD_MIN_Y));

         tracked.m_deltax = (tracked.m_x == Constants.BATTLEFIELD_MIN_X ? Math
               .abs(deltax)
               : Math.abs(deltax) * -1);
         tracked.m_deltay = deltay;
      }

      // Assign a random altitude.
      if (m_random.nextFloat() < SURFACE_LEVEL_CHANCE) {
         tracked.m_z = 0;
      } else {
         tracked.m_z = MIN_ALTITUDE
               + (int) (m_random.nextFloat() * (MAX_ALTITUDE - MIN_ALTITUDE));
      }

      if (DEBUG) {
         output("Created a new tracked object: " + tracked.toString());
      }

      // Add the object to the map.
      Object old = m_tracked.put(new Integer(tracked.m_id), tracked);
      if (old != null) {
         output("Warning: new tracked object " + tracked + " replaced "
               + old + " in the map");
      }

      return tracked;
   }

   public void startUpdates() {
      if (!m_performUpdating) {
         output("Starting updates");
         m_performUpdating = true;
      }
   }

   public void stopUpdates() {
      if (m_performUpdating) {
         output("Stopping updates");
         m_performUpdating = false;
      }
   }

   public void stopUpdateTask() {
      output("Stopping updates");
      m_updater.m_keepGoing = false;

      synchronized (m_updater) {
         m_updater.notify();
      }

      try {
         m_updaterThread.join();
      } catch (InterruptedException ie) {
         output("Join interrupted");
      }

      output("Stop complete");
   }

   public String toString() {
      return "Battlefield";
   }

   protected synchronized void setMessagePaddingMember(int padding,
         DataWriterControl control) {
      m_padding = createPadding(padding);
      output(control.getTitle() + ": message padding set to " + padding);
      control.messagePaddingSet();
   }

   public static void main(String[] args) {
      try {
         // Initialise the Splice application.
         BattlefieldAppWrapper.start();
      } catch (WrapperException e) {
         System.err.println("Cannot start application: " + e.getMessage());
         return;
      }

      // Initialise the application.
      final BattlefieldApp instance = new BattlefieldApp(args);

      //
      // Create the display components.
      //

      // Output panel: displays the tracked objects that the sensor has
      // detected.
      JPanel outputPanel = new JPanel();
      outputPanel.setBorder(BorderFactory.createTitledBorder(m_bevelBorder,
            "Battlefield Objects"));
      outputPanel.setPreferredSize(new Dimension(600, 300));
      outputPanel.setLayout(new BorderLayout());
      m_table = new JTable();
      m_tableModel = new ArrayListTableModel(m_tableHeaders,
            new ArrayList<ArrayList<Integer>>());
      m_table.setModel(m_tableModel);
      JScrollPane sp = new JScrollPane(m_table);
      outputPanel.add(sp, BorderLayout.CENTER);

      // Settings panel.

      Dimension spinnerSize = new Dimension(100, 20);

      SpinnerNumberModel mtModel = new SpinnerNumberModel(m_currentTracked,
            MINIMUM_TRACKED, MAXIMUM_TRACKED, 1);
      JSpinner maxTrackedSpinner = new JSpinner(mtModel);
      maxTrackedSpinner.setPreferredSize(spinnerSize);
      JLabel maxTrackedLabel = new JLabel("Number of tracked objects");

      SpinnerNumberModel ufModel = new SpinnerNumberModel(
            m_battlefieldUpdateDelay, BATTLEFIELD_MINIMUM_UPDATE_DELAY,
            BATTLEFIELD_MAXIMUM_UPDATE_DELAY, 100);
      JSpinner updateFreqSpinner = new JSpinner(ufModel);
      updateFreqSpinner.setPreferredSize(spinnerSize);
      JLabel updateFreqLabel = new JLabel("Delay between updates (ms)");

      JPanel p1 = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      p1.add(maxTrackedLabel);
      p1.add(maxTrackedSpinner);
      JPanel p2 = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      p2.add(updateFreqLabel);
      p2.add(updateFreqSpinner);

      JPanel settingsPanel = new JPanel(new GridLayout(0, 1));
      settingsPanel.setBorder(BorderFactory.createTitledBorder(m_bevelBorder,
            "Battlefield Settings"));
      settingsPanel.add(p1);
      settingsPanel.add(p2);

      DataWriterControl dwc = instance.new DataWriterControl(
            instance.m_objectLocationDataWriter,
            "ObjectLocation Data Writer", DEFAULT_LATENCY_BUDGET,
            DEFAULT_TRANSPORT_PRIORITY, DEFAULT_PADDING);

      // Buttons panel.
      m_startButton = new JButton("Start");
      m_stopButton = new JButton("Stop");
      m_exitButton = new JButton("Exit");
      JPanel innerButtonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      innerButtonPanel.add(m_startButton);
      innerButtonPanel.add(m_stopButton);
      innerButtonPanel.add(m_exitButton);
      JPanel buttonPanel = new JPanel(new BorderLayout(10, 5));
      buttonPanel.add(innerButtonPanel, BorderLayout.SOUTH);

      // Combine data writer and settings panels.
      JPanel controlPanel = new JPanel(new BorderLayout(10, 5));
      controlPanel.add(dwc.getPanel(), BorderLayout.NORTH);
      controlPanel.add(settingsPanel, BorderLayout.SOUTH);

      // Combine JVM panel with the above.
      JPanel combined = new JPanel(new GridLayout(1, 2));
      combined.add(instance.m_jvmControl.getPanel());
      combined.add(controlPanel);

      // Combine button panel with the above.
      JPanel combined2 = new JPanel(new BorderLayout(10, 5));
      combined2.add(combined, BorderLayout.NORTH);
      combined2.add(buttonPanel, BorderLayout.SOUTH);

      // Add button action listeners.
      m_startButton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent ae) {
            instance.startUpdates();
            m_startButton.setEnabled(false);
            m_stopButton.setEnabled(true);
         }
      });
      m_stopButton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent ae) {
            instance.stopUpdates();
            m_startButton.setEnabled(true);
            m_stopButton.setEnabled(false);
         }
      });
      m_exitButton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent ae) {
            m_startButton.setEnabled(false);
            m_stopButton.setEnabled(false);
            m_exitButton.setEnabled(false);
            instance.stopUpdates();
            instance.stopUpdateTask();
            instance.stopJVMInformationUpdates();
         }
      });

      // Add spinner action listeners.
      maxTrackedSpinner.addChangeListener(new ChangeListener() {
         public void stateChanged(ChangeEvent ce) {
            JSpinner spinner = (JSpinner) ce.getSource();
            Integer val = (Integer) spinner.getValue();
            m_currentTracked = val.intValue();

            if (instance.DEBUG) {
               instance.output("Current max tracked object count set to "
                     + m_currentTracked);
            }
         }
      });
      updateFreqSpinner.addChangeListener(new ChangeListener() {
         public void stateChanged(ChangeEvent ce) {
            JSpinner spinner = (JSpinner) ce.getSource();
            Integer val = (Integer) spinner.getValue();
            m_battlefieldUpdateDelay = val.intValue();

            if (instance.DEBUG) {
               instance.output("Battlefield update delay set to "
                     + m_battlefieldUpdateDelay);
            }
         }
      });

      // Set up initial interface state.
      m_stopButton.doClick();
      dwc.setTransportPriority(DEFAULT_TRANSPORT_PRIORITY);
      dwc.setLatencyBudget(DEFAULT_LATENCY_BUDGET);
      dwc.setMessagePadding(DEFAULT_PADDING);

      //
      // Construct the frame.
      //

      m_frame = new JFrame(instance.toString());
      m_frame.addWindowListener(new WindowAdapter() {
         public void windowClosing(WindowEvent e) {
            m_exitButton.doClick();
         }
      });

      m_frame.getContentPane().setLayout(new BorderLayout(10, 10));
      m_frame.getContentPane().add(outputPanel, BorderLayout.NORTH);
      m_frame.getContentPane().add(combined2, BorderLayout.SOUTH);

      m_frame.pack();
      m_frame.setVisible(true);

      // Automatically start if required.
      if (AUTO_START) {
         launchStartThread(5000L, m_startButton);
      }

      // Join on the update thread to keep active.
      try {
         instance.m_updaterThread.join();
      } catch (InterruptedException ie) {
         // Ignore.
      }

      try {
         // Stop the Splice application.
         BattlefieldAppWrapper.stop();
      } catch (WrapperException e) {
         System.err.println("Cannot stop application: " + e.getMessage());
         return;
      }

      // Complete.
      System.out.println("BattlefieldApplication.main :: exiting");
      m_frame.dispose();
   }
}
